oat++でSwaggerのUI/Specを自動生成する!
はじめに
福岡のyoshihitohです。前回の記事で試した oat++ にSwagger関連の機能が用意されていたので試してみました。
検証環境
- macOS: 10.13.6
- Command Line Tools for Xcode: 10.0.0.0.1.1535735448
- Premake: 5.0 alpha 13
- CMake: 3.9.0
試してみる
前回のコードをベースにSwaggerの機能を組み込んでいきます。
プロジェクト全体のソースは下記リポジトリに置いてあります。
oatpp-swaggerを組み込む
Swagger関連の機能は別モジュールになっているのでGitHubからクローンしてきます。
$ cd external $ git clone https://github.com/oatpp/oatpp-swagger
oatpp-swaggerはビルド用のスクリプトが用意されていないようです。前回作ったpremakeの設定ファイルにビルドルールを追加します。
+project "oatpp-swagger" + kind "StaticLib" + language "C++" + + includedirs { + "external", + } + + files { + "external/oatpp-swagger/**.cpp", + "external/oatpp-swagger/**.hpp", + } + project "oatpp-book" kind "ConsoleApp" language "C++
oatpp-bookプロジェクトの設定を変更して、 oatpp-swagger
をリンクするようにします。
project "oatpp-book" kind "ConsoleApp" language "C++" @@ -36,5 +49,6 @@ project "oatpp-book" } links { - "oatpp" + "oatpp", + "oatpp-swagger" }
ここまででoatpp-swaggerの組み込み準備は完了です。簡単ですね。
エンドポイントの情報を設定する
BookControllerのエンドポイントの情報を設定します。前回は ENDPOINT
マクロでエンドポイントを実装しました。今回は ENDPOINT_INFO
でエンドポイントの概要やリクエスト・レスポンスの情報を設定します。
#include OATPP_CODEGEN_BEGIN(ApiController) + ENDPOINT_INFO(allBooks) { + info->summary = "すべての本を取得する"; + info->addResponse<decltype(m_database->allBooks())>(Status::CODE_200, "application/json"); + } ENDPOINT("GET", "api/books", allBooks) { return createDtoResponse(Status::CODE_200, m_database->allBooks()); } + ENDPOINT_INFO(getBook) { + info->summary = "IDを指定して本を取得する"; + info->addResponse<BookDto::ObjectWrapper>(Status::CODE_200, "application/json"); + info->addResponse<String>(Status::CODE_404, "text/plain"); + } ENDPOINT("GET", "api/books/{book_id}", getBook, PATH(Int32, book_id)) { const auto book = m_database->getBook(book_id); OATPP_ASSERT_HTTP(book, Status::CODE_404, "book not found"); return createDtoResponse(Status::CODE_200, book); } + ENDPOINT_INFO(putBook) { + info->summary = "IDを指定して本を更新する"; + info->addConsumes<BookDto::ObjectWrapper>("application/json"); + info->addResponse<BookDto::ObjectWrapper>(Status::CODE_200, "application/json"); + info->addResponse<String>(Status::CODE_404, "text/plain"); + } ENDPOINT("PUT", "api/books/{book_id}", putBook, PATH(Int32, book_id), BODY_DTO(BookDto::ObjectWrapper, book_dto)) {
サマリは日本語情報を使えて、リクエスト・レスポンスの形式は型情報を与えるだけで良いみたいです。楽できてありがたいですね!
Swaggerコンポーネントを作る
前回も参考にした crud の SwaggerComponent を参考に実装します。
#pragma once #include "oatpp/core/macro/component.hpp" #include "oatpp-swagger/Model.hpp" #include "oatpp-swagger/Resources.hpp" class SwaggerComponent { public: OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::swagger::DocumentInfo>, swagger_document_info)([] { oatpp::swagger::DocumentInfo::Builder builder; builder .setTitle("Book management service") .setDescription("Book API Example project with swagger docs") .setVersion("1.0") .setContactName("yoshihitoh") .setContactUrl("https://classmethod.jp") .addServer("http://localhost:8002", "server on localhost"); return builder.build(); }()); OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::swagger::Resources>, swagger_resources)([] { return oatpp::swagger::Resources::loadResources("./external/oatpp-swagger/res"); }()); };
Swaggerドキュメントの設定と、Webページのレンダリングで使うリソースのパスを指定します。今回はルートディレクトリから実行する前提の相対パスを指定しています。
SwaggerComponentを有効化する
最後にSwaggerのコンポーネントを組み込んで、コントローラを有効化します。
まず、 AppComponent
に SwaggerComponent
を追加します。
#include "db/database.hpp" #include "db/memory_database.hpp" +#include "swagger_component.hpp" class AppComponent { public: + SwaggerComponent swagger_component; + OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::network::ServerConnectionProvider>, serverConnectionProvider)([] { return oatpp::network::server::SimpleTCPConnectionProvider::createShared(8002); }());
次にmain関数の起動処理で、Swaggerのコンポーネント・コントローラを有効化します。
#include <iostream> +#include "oatpp-swagger/Controller.hpp" + #include "controller/book_controller.hpp" #include "app_component.hpp" #include "logger.hpp" @@ -15,6 +17,12 @@ static void run() auto book_controller = BookController::createShared(); book_controller->addEndpointsToRouter(router); + auto doc_endpoints = oatpp::swagger::Controller::Endpoints::createShared(); + doc_endpoints->pushBackAll(book_controller->getEndpoints()); + + auto swagger_controller = oatpp::swagger::Controller::createShared(doc_endpoints); + swagger_controller->addEndpointsToRouter(router); + oatpp::network::server::Server server(components.serverConnectionProvider.getObject(), components.serverConnectionHandler.getObject()); OATPP_LOGD("Server", "Running on port %u...", components.serverConnectionProvider.getObject()->getPort());
以上でSwagger機能の準備は完了です。
動作確認
ビルドして起動する
実際に動かしてみましょう。前回と同様の手順でビルドします。
# プロジェクトファイル(Makefile)を更新 $ premake5 gmake2 # ビルド $ make -j$(sysctl -n hw.ncpu)
私の環境だとこの指定でビルドすると下記のエラーが発生しました。
$ make clean && make -j$(sysctl -n hw.ncpu) ... ==== Building oatpp-book (debug) ==== Creating obj/Debug/oatpp-book book.cpp main.cpp memory_database.cpp Linking oatpp-book ld: warning: ignoring file bin/Debug/liboatpp-swagger.a, file was built for archive which is not the architecture being linked (x86_64): bin/Debug/liboatpp-swagger.a Undefined symbols for architecture x86_64:
oatpp-swaggerを静的ライブラリとしてビルドするときに、binutilsの ar
と ranlib
でアーカイブしてしまったようです。下記の記事を参考にパスを明示することで解消できました。
$ make clean && AR="/usr/bin/ar" RANLIB="/usr/bin/ranlib" make -j$(sysctl -n hw.ncpu) ... ==== Building oatpp-book (debug) ==== Creating obj/Debug/oatpp-book book.cpp main.cpp memory_database.cpp Linking oatpp-book bash-4.4$
前回と同様に起動します。今回はSwaggerのリソースパスを相対パスで指定しているので、ルートディレクトリから起動します。
$ ./build-gmake/bin/Debug/oatpp-book Server:Running on port 8002...
Swagger-UIを確認する
起動したらSwagger-UIのページ http://localhost:8002/swagger/ui
にアクセスしてみます。
ちゃんとエンドポイントの定義が追加されていますね!試しにPUTの詳細を見てみると
パスとリクエストのパラメタについてちゃんと反映されています。
レスポンスの情報も設定したとおりですね。
DTOオブジェクトのスキーマもばっちりです。
Swagger-Specを確認する
http://localhost:8002/api-docs/oas-3.0.0.json
にアクセスしてSwagger-Specを出力します。JSON形式で出力できるんですが、書式が少し冗長なのでYAML形式に変換してみます。JSON→YAMLの変換は下記のエントリを参考にRubyのワンライナーを使います。
$ curl -XGET http://localhost:8002/api-docs/oas-3.0.0.json | ruby -ryaml -rjson -e 'puts YAML.dump(JSON.parse(STDIN.read))' % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1876 100 1876 0 0 267k 0 --:--:-- --:--:-- --:--:-- 305k --- openapi: 3.0.0 info: title: Book management service description: Book API Example project with swagger docs contact: name: yoshihitoh url: https://classmethod.jp version: '1.0' servers: - url: http://localhost:8002 description: server on localhost paths: "/api/books": get: summary: すべての本を取得する operationId: allBooks responses: '200': description: OK content: application/json: schema: type: array items: "$ref": "#/components/schemas/BookDto" parameters: [] "/api/books/{book_id}": get: summary: IDを指定して本を取得する operationId: getBook responses: '404': description: Not Found content: text/plain: schema: type: string '200': description: OK content: application/json: schema: "$ref": "#/components/schemas/BookDto" put: summary: IDを指定して本を更新する operationId: putBook requestBody: description: request body content: application/json: schema: "$ref": "#/components/schemas/BookDto" responses: '404': description: Not Found content: text/plain: schema: type: string '200': description: OK content: application/json: schema: "$ref": "#/components/schemas/BookDto" parameters: - name: book_id in: path required: true schema: type: integer format: int32 components: schemas: BookDto: type: object properties: id: type: integer format: int32 title: type: string author: type: string isbn: type: string publish_at: type: integer format: int64
こちらも問題なく、設定した通りに出力されていますね!
おわりに
今回は oatpp-swagger を利用して、Swagger-UIとSwagger-Specを自動生成してみました。今までWAFのSwagger統合機能を使ったことがなかったので衝撃的でした。私は知らなかったのですが、Swagger統合をサポートしているWAFや同等のことを実現するツールが沢山あるようです。
Swagger-SpecとAPIの実装を同じコードで管理できると定義と実装の乖離を防げてメンテが楽になりそうですね。機会があれば業務でも活用していきたいと思います。